﻿using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Web;
using System.Web.Routing;
using System.Web.Caching;
using System.Web.Mvc;
using Sencha.Serialization;

using DescriptionCache = Sencha.Direct.Api.DescriptionCache;

namespace Sencha.Direct
{
	public abstract partial class Router : DirectController, IHttpHandler
	{
		private class DirectRequestResult : IJsonSerializable
		{
			private Encoding m_encoding;
			private MemoryStream m_stream;
			private bool m_isJson;

			public DirectRequestResult(MemoryStream stream, Encoding encoding, bool isJson)
			{
				m_stream = stream;
				m_encoding = encoding;
				m_isJson = isJson;
			}

			#region IJsonSerializable Members

			public void ReadJson(IJsonReader reader)
			{
				throw new NotSupportedException();
			}
			public void WriteJson(IJsonWriter writer)
			{
				m_stream.Position = 0;
				var str = new StreamReader(m_stream, m_encoding).ReadToEnd();
				if (m_isJson)
					writer.WriteJson(str);
				else
					writer.Write( str);
			}

			#endregion
		}

		public static string TokenRoutedBy = "RoutedBy";
		public static string TokenParentControllerContext = "ParentControllerContext";


		[NonDirectAction]
		public ActionResult Route()
		{
			try
			{
				var formAction = HttpContext.Request[IncomingRequest.RequestFormAction];
				if (!string.IsNullOrEmpty(formAction))
					return RouteFormRequest();
				else
				{
#if TRACE
					System.Diagnostics.Debug.Write("[Direct <-]");
					System.Diagnostics.Debug.Write(string.Format("({0} b)", Request.InputStream.Length));
					System.Diagnostics.Debug.WriteLine(new System.IO.StreamReader(Request.InputStream).ReadToEnd());
					Request.InputStream.Position = 0;
#endif
					JsonResult res = (JsonResult)RouteJsonRequest();
#if TRACE
					var data = Sencha.Serialization.Json.SerializeToString(res.Data);
					System.Diagnostics.Debug.Write("[Direct ->]");
					System.Diagnostics.Debug.Write(string.Format("({0} sym)", data.Length));
					System.Diagnostics.Debug.WriteLine(data);

#endif
					return res;
				}
			}
			#region Error handling
			catch (System.Threading.ThreadAbortException) { throw; }
			catch (OutOfMemoryException) { throw; }
			catch (Exception exc)
			{
				return RaiseRoutingException(exc);
			}
			#endregion
		}

		private ActionResult RouteFormRequest()
		{
			// capture form POST
			var request = IncomingRequest.Capture(HttpContext);
			// check type
			if (request.Type != "rpc")
				return RaiseRequestException(new Exceptions.UnsupportedRequestTypeException());

			var compress = false;
			var responseResult = (object)null;
			var responseError = (JsError)null;
			try
			{
				// create context
				var context = new RoutingContext(request);
				// pre-request event
				this.RequestExecuting(context);
				// execution
				if (context.RequestResult == null)
					context.RequestResult = responseResult = InvokeAction(context);
				// post-request event
				this.RequestExecuted(context);
				// apply post-request changes
				responseResult = context.RequestResult;

				compress = compress | DirectJsonResult.IsCompressionEnabled(responseResult);
			}
			#region Error handling
			catch (System.Threading.ThreadAbortException) { throw; }
			catch (OutOfMemoryException) { throw; }
			catch (Exception exc)
			{
				responseError = (JsError)this.RaiseRequestException(exc).Data;
				responseResult = null;
			}
			#endregion
			var response = new OutgoingResponse(request, responseResult, responseError);
			// that was standart in Ext.Direct spec
			/*
			var serializedContent = new System.Text.StringBuilder(5 * 1024);
			var serializer = this.CreateSerializer(typeof(DirectResponse));
			serializer.WriteObject(serializedContent, response );

			serializedContent.Replace("&quot;", "\\&quot;");
			serializedContent.Insert(0, "<html><body><textarea>");
			serializedContent.Append("</textarea></body></html>");
			*/

			return TypedJson(response, compress);
		}
		private ActionResult RouteJsonRequest()
		{
			// deserialize requests
			var requests = DeserializeRequests(Request.InputStream);
			var responses = new List<OutgoingResponse>(requests.Length);

			var compress = false;

			// execute requests
			foreach (var request in requests)
			{
				var responseResult = (object)null;
				var responseError = (JsError)null;

				if (request.Type != "rpc")
					return RaiseRequestException(new Exceptions.UnsupportedRequestTypeException());

				//if (request.ParameterNames == null || // forgot param names
				//    request.Arguments == null && request.ParameterNames.Length != 0 || // missing arguments
				//    (request.Arguments != null && request.ParameterNames != null &&
				//    request.ParameterNames.Length != request.Arguments.Length)) // arguments length != params length
				//{
				//    return RaiseRequestException(new Exceptions.InvalidRequestParameters());
				//}

				if (request.Arguments == null)
					return RaiseRequestException(new Exceptions.InvalidRequestParameters());

				try
				{
					// create context
					var context = new RoutingContext(request);
					// pre-request event
					this.RequestExecuting(context);
					// execution
					if (context.RequestResult == null)
						context.RequestResult = responseResult = InvokeAction(context);
					// post-request event
					this.RequestExecuted(context);
					// apply post-request changes
					responseResult = context.RequestResult;

					compress = compress || DirectJsonResult.IsCompressionEnabled(responseResult);
				}
				#region Error handling
				catch (System.Threading.ThreadAbortException) { throw; }
				catch (OutOfMemoryException) { throw; }
				catch (Exception exc)
				{
					responseError = (JsError)this.RaiseRequestException(exc).Data;
					responseResult = null;
				}
				#endregion

				// add responses to result list
				responses.Add(new OutgoingResponse(request, responseResult, responseError));
			}

			// return responses
			return TypedJson(responses, compress);
		}
		private IncomingRequest[] DeserializeRequests(System.IO.Stream inputStream)
		{
			if (inputStream == null)
				throw new ArgumentNullException("inputStream");

			try
			{
				var serializer = this.CreateSerializer(typeof(IncomingRequest[]));
				return (IncomingRequest[])serializer.ReadObject(inputStream);
			}
			catch (InvalidCastException exc)
			{
				throw new Exceptions.RequestsDeserializationFailed(exc);
			}
			catch (System.Runtime.Serialization.SerializationException exc)
			{
				throw new Exceptions.RequestsDeserializationFailed(exc);
			}
		}

		private IJsonSerializable InvokeAction(RoutingContext context)
		{
			if (context == null)
				throw new ArgumentNullException("context");

			var reqContext = this.PrepareRequestContext(context);
			var httpContext = reqContext.HttpContext;
			var controller = ControllerBuilder.Current.GetControllerFactory().CreateController(reqContext, context.Controller) as IController;

			if (controller == null || controller is Router)
				throw new HttpException(404, Loc.GetErrorString("Error_ControllerIsNotFound", context.Controller));

			if (controller is Controller)
			{
				var contr = (controller as Controller);
				var controllerContext = new ControllerContext(reqContext, controller as ControllerBase);
				contr.ControllerContext = controllerContext;
				if (contr.ActionInvoker is ControllerActionInvoker)
					contr.ActionInvoker = new RouterActionInvoker();

				// not needed yet
				//var metaInformation = DescriptionCache.GetControllerDescription(controller.GetType());
				//var actionDescriptor = metaInformation.Descriptor.FindAction(controllerContext, context.Action);
				//var actionDescription = DescriptionCache.GetActionDescription(actionDescriptor);

				//if (actionDescription.AcceptVerbs != null && actionDescription.AcceptVerbs.Count > 0)
				//    FakeHttpRequest.SetHttpMethod(httpContext.Request, actionDescription.AcceptVerbs);
			}
			controller.Execute(reqContext);

			if (httpContext.Response.StatusCode != 200)
				throw new HttpException(httpContext.Response.StatusCode, httpContext.Response.StatusDescription);

			var isJson = httpContext.Response.ContentType == "application/json";
			var responseStream = (httpContext.Response as FakeHttpResponse).OutputStream as MemoryStream;
			return new DirectRequestResult(responseStream, httpContext.Response.ContentEncoding, isJson);
		}
		private IAsyncResult InvokeActionAsync(RoutingContext context)
		{
			if (context == null)
				throw new ArgumentNullException("context");

			var reqContext = this.PrepareRequestContext(context);
			var httpContext = reqContext.HttpContext;
			var controller = ControllerBuilder.Current.GetControllerFactory().CreateController(reqContext, context.Controller) as IController;

			if (controller == null || controller is Router)
				throw new HttpException(404, Loc.GetErrorString("Error_ControllerIsNotFound"));

			if (controller is ControllerBase)
			{
				var controllerContext = new ControllerContext(reqContext, controller as ControllerBase);
				(controller as ControllerBase).ControllerContext = controllerContext;
			}

			var asyncController = (controller as System.Web.Mvc.Async.IAsyncController);
			if (asyncController != null)
			{
				return asyncController.BeginExecute(reqContext, (r) =>
				{
					asyncController.EndExecute(r);

					if (httpContext.Response.StatusCode != 200)
						throw new HttpException(httpContext.Response.StatusCode, httpContext.Response.StatusDescription);

					var isJson = httpContext.Response.ContentType == "application/json";
					var responseStream = (httpContext.Response as FakeHttpResponse).OutputStream as MemoryStream;

					context.RequestResult = new DirectRequestResult(responseStream, httpContext.Response.ContentEncoding, isJson);

				}, null);
			}
			else
			{
				var asyncExecuteAction = new Action<RequestContext>(controller.Execute);
				return asyncExecuteAction.BeginInvoke(reqContext, (r) =>
				{
					asyncExecuteAction.EndInvoke(r);

					if (httpContext.Response.StatusCode != 200)
						throw new HttpException(httpContext.Response.StatusCode, httpContext.Response.StatusDescription);

					var isJson = httpContext.Response.ContentType == "application/json";
					var responseStream = (httpContext.Response as FakeHttpResponse).OutputStream as MemoryStream;

					context.RequestResult = new DirectRequestResult(responseStream, httpContext.Response.ContentEncoding, isJson);

				}, null);
			}
		}
		private RequestContext PrepareRequestContext(RoutingContext context)
		{
			if (context == null)
				throw new ArgumentNullException("context");

			var hostUrl = this.Request.Url.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped);

			var routeValues = new RouteValueDictionary(this.RouteData.Values);
			routeValues["action"] = context.Action;

			if (!string.IsNullOrEmpty(context.Controller))
				routeValues["controller"] = context.Controller;

			var emptyCollection = new NameValueCollection();
			var requestArguments = new NameValueCollection(context.Parameters.Count);
			foreach (var arg in context.EnumerateArguments())
				requestArguments.Add(arg.Key, arg.Value);

			var orginalContext = this.HttpContext;
			var responseStream = new MemoryStream(100 * 1024);
			HttpContextBase httpContext = new FakeHttpContext(orginalContext,
				new FakeHttpRequest(orginalContext.Request)
				{
					RawUrlOverride = hostUrl + this.Url.Action(context.Action, context.Controller),
					FormOverride = requestArguments,
					QueryStringOverride = emptyCollection,
					ParamsOverride = requestArguments
				},
				new FakeHttpResponse(orginalContext.Response, responseStream)
			);

			var data = RouteTable.Routes.GetVirtualPath(this.ControllerContext.RequestContext, routeValues);
			if (data == null)
				throw new HttpException(404, Loc.GetErrorString("Error_NoRouteMatched"));

			RouteData routeData = new RouteData();
			foreach (KeyValuePair<string, object> pair in routeValues)
				routeData.Values.Add(pair.Key, pair.Value);
			foreach (KeyValuePair<string, object> pair2 in data.DataTokens)
				routeData.DataTokens.Add(pair2.Key, pair2.Value);

			routeData.Values.Remove("area");

			routeData.Route = data.Route;
			routeData.DataTokens["area"] = this.RouteData.DataTokens["area"];
			routeData.DataTokens[TokenParentControllerContext] = this.ControllerContext;
			routeData.DataTokens[TokenRoutedBy] = this;

			var reqContext = new RequestContext(httpContext, routeData);

			return reqContext;
		}

		private ErrorActionResult RaiseRoutingException(Exception exception)
		{
			var context = new ExceptionContext(this.ControllerContext, exception);
			// callvirt
			this.OnRoutingException(context);

			context.Result = this.ConvertToErrorActionResult(context.Result, exception);

			return (context.Result as ErrorActionResult);
		}
		private ErrorActionResult RaiseRequestException(Exception exception)
		{
			var context = new ExceptionContext(this.ControllerContext, exception);
			// callvirt
			this.OnRoutingException(context);

			context.Result = this.ConvertToErrorActionResult(context.Result, exception);

			return (context.Result as ErrorActionResult);
		}
		private ErrorActionResult ConvertToErrorActionResult(ActionResult actionResult, Exception exception)
		{
			if (!(actionResult is ErrorActionResult))
			{
#if DEBUG
				var error = new JsError(exception); // error with full strack trace
#else
				var error = new JsError(); // generic error
				if (exception is HttpException)
				{
					error.Message = (exception as HttpException).Message;
					error.Name = "Http exception";
					error.HelpUrl = string.Concat("http://en.wikipedia.org/wiki/HTTP_", (exception as HttpException).ErrorCode);
				}
#endif
				return new ErrorActionResult(error);
			}
			else if (actionResult is JsonResult && (actionResult as JsonResult).Data is JsError)
			{
				return new ErrorActionResult((JsError)(actionResult as JsonResult).Data);
			}

			return (ErrorActionResult)actionResult;
		}

		protected virtual void OnRoutingException(ExceptionContext context) { }
		protected virtual void OnRequestException(ExceptionContext context)
		{
			// pass-through RdpConnection exception
			if (context.Exception is Exceptions.RemoteInvocationException)
			{
				var error = (context.Exception as Exceptions.RemoteInvocationException).OriginalError;
#if DEBUG
				error.Description = string.Format("Exception chained at '{0}'\r\n", this.HttpContext.Request.RawUrl) + error.Description ?? "";
#endif
				context.Result = new ErrorActionResult(error);
			}
		}
		protected virtual void RequestExecuted(RoutingContext context) { }
		protected virtual void RequestExecuting(RoutingContext context) { }

		#region IHttpHandler
		bool IHttpHandler.IsReusable
		{
			get { return true; }
		}
		void IHttpHandler.ProcessRequest(HttpContext context)
		{

			if (this.ControllerContext == null) // no context = un-routed
			{
				// rewrite path
				var path = RouterHelpers.DefaultRoute.Url;
				context.RewritePath(path);

				// create request(routing) context
				var httpContext = new HttpContextWrapper(context);
				var routeData = new RouteData(RouterHelpers.DefaultRoute, RouterHelpers.DefaultRoute.RouteHandler);
				var requestContext = new RequestContext(httpContext, routeData);

				// create controller context
				this.ControllerContext = new ControllerContext(requestContext, this);
				this.Url = new UrlHelper(requestContext);
			}

			// call route
			var actionResult = this.Route();
			if (actionResult == null)
				actionResult = new EmptyResult();

			// execute result
			actionResult.ExecuteResult(this.ControllerContext);
		}
		#endregion
	}
}
